swift 闭包(闭包表达式、尾随闭包、逃逸闭包、自动闭包)

8,253 阅读5分钟

这是我参与更文挑战的第8天,活动详情查看: 更文挑战

闭包

  • 闭包是独立的函数代码块,可以在代码中被传递和使用
  • 闭包和block的对比
    • swift中闭包与OC的 block 比较相似
    • swift中闭包是一个特殊函数,OC中block是一个匿名函数
    • 闭包和block都经常用于回调
    • block表达式
      类型:返回值类型(^block的名称)(block的参数)
      
      返回值(^block的名称)(形参列表) =  ^( 形参列表){
          // 执行代码
      };
      
      
      NSString* (^blockName)(NSString *str) = ^(NSString *str){
          return str;
      };
      
      blockName(@"block")
      
    • 闭包表达式
      类型:(参数)->(返回值类型)
      
      闭包名称 = { (形参列表) -> return 返回值 in
          // 执行代码  
      }
      let closure = { (str:String) -> String in
          return str
      }
      
      closure("closure")
      

闭包类型

  • 无参数无返回值
    let closure = {()->Void in
         print("closure")
    }
    
    closure()
    
  • 有参数无返回值
    let closure = {(str:String) -> Void in
        print(str)
    }
    
    closure("closure")
    
  • 无参数有返回值
    let closure = {() -> String in
        return "closure"
    }
    
    closure()
    
  • 有参数有返回值
    let closure = { (str:String) -> String in
        return str
    }
    
    closure("closure")
    

闭包表达式

下面例子通过使用几次迭代展示了 sorted(by:) 方法的定义和语法优化的方式。每一次迭代都用更简洁的方式描述了相同的功能

  • 通过函数处理

    sorted(by:) 方法接受一个闭包,该闭包函数需要传入与数组元素类型相同的两个值,并返回一个布尔值来进行排序。排序闭包函数类型需为:(Int, Int) -> Bool

    let numbers = [1,9,2,8,3,7,4,6];
    let numArr = numbers.sorted(by: callback)
    func callback(_ numA:Int, _ numB:Int) -> Bool {
        return numA < numB
    }
    print(numArr)    
    
    log:
    [1, 2, 3, 4, 6, 7, 8, 9]
    
  • 通过闭包表达式处理

    内联闭包参数和返回值类型声明与callback(_:_:)函数类型声明相同

    let numbers = [1,9,2,8,3,7,4,6];
    let numArr = numbers.sorted { (numA:Int, numB:Int) -> Bool in
        return numA < numB
    }
    print (numArr) 
    
    log:
    [1, 2, 3, 4, 6, 7, 8, 9]
    
  • 根据上下文推断类型

    sorted(by:)方法被一个Int数组调用,因此其参数必须是 (Int, Int) -> Bool类型的函数,因为所有的类型都可以被正确推断,所以返回箭头(->)和围绕在参数周围的括号也可以被省略

    let numbers = [1,9,2,8,3,7,4,6];
    let numArr = numbers.sorted { numA,numB in 
        return numA < numB 
    }
    print(numArr)
    
    log
    [1, 2, 3, 4, 6, 7, 8, 9]
    
  • 单表达式闭包隐式返回

    sorted(by:)方法的参数类型明确了闭包必须返回一个Bool 类型值,因为闭包函数体只包含了一个单一表达式(s1 > s2),该表达式返回Bool类型值,因此这里没有歧义,return关键字可以省略

    let numbers = [1,9,2,8,3,7,4,6];
    let numArr = numbers.sorted { numA,numB in numA < numB }
    print(numArr)    
    
    log:
    [1, 2, 3, 4, 6, 7, 8, 9]
    
  • 参数名称缩写

    可以直接通过$0,$1,$2 来顺序调用闭包的参数,以此类推。如果你在闭包表达式中使用参数名称缩写,你可以在闭包定义中省略参数列表,并且对应参数名称缩写的类型会通过函数类型进行推断。in关键字也同样可以被省略,因为此时闭包表达式完全由闭包函数体构成

    let numbers = [1,9,2,8,3,7,4,6];
    let numArr = numbers.sorted {$0 < $1}
    print(numArr)
    
    log:
    [1, 2, 3, 4, 6, 7, 8, 9]
    
  • 运算符方法

    swift 的 Int类型定义了关于大于号(>)的实现,其作为一个函数接受两个Int类型的参数并返回Bool类型的值。而这正好与sorted(by:)方法的参数需要的函数类型相符合。因此,可以简单地传递一个大于号

    let numbers = [1,9,2,8,3,7,4,6];
    let numArr = numbers.sorted(by: <)
    print(numArr)
    
    log:
    [1, 2, 3, 4, 6, 7, 8, 9]
    

尾随闭包

一个无名闭包作为一个参数传给一个普通的函数

  • 如果闭包是函数的最后一个参数,那么调用的时候将闭包写在括号()后面

    func sayHi(str1:String, str2:String, closure:(String,String)->String)->String{
        return closure(str1,str2);
    }
    
    let message = sayHi(str1: "hello", str2: "world") { (str1:String, str2:String)-> (String) in
        return str1 + " " + str2
    }
    
    print(message)
    
    log:
    hello world
    
  • 如果函数只有一个参数且为闭包,那么调用的时候括号()可以不写

    func sayHi(closure:(String)->())-> Void {
    
        print("只有一个参数哦")
        closure("hello world")
    }
    
    sayHi { (str:String)  in
        print(str)
    }
    
    log:
    只有一个参数哦
    hello world
    

值捕获

闭包可以在其被定义的上下文中捕获常量或变量。即使定义这些常量和变量的原作用域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值。

func add(num:Int) -> ()->Int {
    var value = 0
    func result() -> Int{
        value += num
        return value
    }
    /*
     这有一个叫做 add 的函数,其包含了一个叫做 result 的嵌套函数
     嵌套函数result()从上下文中捕获了两个值,value 和 num
     捕获这些值之后,add 将 result 作为闭包返回
     每次调用 result 时,其会以 num 作为增量增加 value 的值
     */
    return result
}
  • 嵌套函数可以捕获其外部函数所有的参数以及定义的常量和变量,并且保证了在下一次执行函数时,之前捕获的值依旧存在

    func add(num:Int) -> ()->Int {
        var value = 0
        func result() -> Int{
            value += num
            return value
        }
        return result
    }
    
    let result = add(num: 10)
    print(result())  //10
    print(result())  //20
    print(result())  //30
    
  • 同一个方法中的变量, 会被绑定到属于自己的变量

    func add(num:Int) -> ()->Int {
        var value = 0
        func result() -> Int{
            value += num
            return value
        }
        return result
    }
    
    
    //如果你创建了另一个result1,它会有属于自己的引用,指向一个全新、独立的value变量
    let result1 = add(num: 10)
    print(result1())  //10
    
    //再次调用原来的result会继续增加它自己的value变量,该变量和result1中捕获的变量没有任何联系
    print(result())  //40
    

闭包是引用类型

  • 函数和闭包都是引用类型

    将函数或闭包赋值给一个常量或变量,实际上都是将常量或变量的值设置为对应函数或闭包的引用

    //这两个常量或变量都引用相同的闭包
    let method = result
    

逃逸闭包

  • 一个传入函数的闭包如果在函数执行结束之后才会被调用,那么这个闭包就叫做逃逸闭包。通俗点讲,不在当前方法中使用闭包,而是在方法之外使用
  • 定义函数的参数为逃逸闭包时,只需要在参数名之前标注 @escaping,用来指明这个闭包是允许“逃逸”出这个函数的
  • 将一个闭包标记为 @escaping意味着你必须在闭包中显式地引用self
    var result: ()->Void = {}
    var str = ""
    func showA(closure: @escaping () -> Void) {
        result = closure
    }
    func showB(closure: () -> Void) {
        closure()
    }
    func doSomething() {
        showA {str = "我是逃逸闭包"}
        showB {str = "我是普通闭包"}
    }
    
    doSomething()
    print(str)    //我是普通的闭包
    result()
    print(str)    //我是逃逸的闭包
    
    逃逸闭包是在函数执行之后再执行,于是这段代码最后输出“我是逃逸的闭包”

自动闭包

  • 自动闭包: 自动创建一个闭包用来包裹一个表达式,这种闭包不接受任何参数,当闭包被调用时,返回包裹在闭包中的表达式的值

  • 自动闭包让你能够延迟求值,因为直到你调用这个闭包,代码段才会被执行

  • 这种便利语法让你能够省略闭包的花括号,用一个普通的表达式来代替显式的闭包

    var arr = ["a","b","c"]
    print(arr.count)  //3
    let closure = {
        arr.remove(at: 0)
    }
    
    print(arr.count)  //3
    print(closure())  //a
    print(arr.count)  //2
    
  • 将闭包作为参数传递给函数时,同样可以延时求值

    函数接受一个显式闭包类型的参数

    func delete(closure: ()->String){
        print(closure())
    }
    
    var arr = ["a","b","c"]
    delete(closure:{arr.remove(at: 0)})    
    
    log:
    a
    

    通过将参数标记为@autoclosure 来接收一个自动闭包,该函数接受的是String类型参数而非闭包

    func delete(closure: @autoclosure ()-> String){
        print(closure())
    }
    
    var arr = ["a","b","c"]
    delete(closure:arr.remove(at: 0))  
    
    log:
    a